/*
Copyright (c) 2013 Mihail Volkov

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include "htmd.h"

#ifndef DISABLE_HTMD

#if (HTMD_MAX_PACKET_SIZE<24)
#error HTMD_MAX_PACKET_SIZE is too small
#endif
#include <cassert>

#include <stdio.h>

//
// chunks
//

char HtMdConstChunkDesc::operator [] (int index) const
{
	assert (index>=0 && index<mLen);
	return mpBuf[index];
}

HtMdConstChunkDesc HtMdConstChunkDesc::ConstSubchunk
	(int startAt,int len) const
{
	if (len<0) len = mLen-startAt;
	assert (startAt>=0 && startAt+len<=mLen);
	return HtMdConstChunkDesc (mpBuf+startAt,len);
}

char &HtMdChunkDesc::operator [] (int index)
{
	assert (index>=0 && index<mLen);
	return mpBuf[index];
}

HtMdChunkDesc HtMdChunkDesc::Subchunk
	(int startAt,int len)
{
	if (len<0) len = mLen-startAt;
	assert (startAt>=0 && startAt+len<=mLen);
	return HtMdChunkDesc (mpBuf+startAt,len);
}

// outgoing low level command codes

// application side (here):
// say hello. sent on startup, tells the client value of this
// app's HTMD_MAX_PACKET_SIZE and protocol version so that
// the client could adjust itself for proper communication
#define LLC_APP_HELLO 0 // params: int 'HTMD',
// ^int HTMD_MAX_PACKET_SIZE, int protoVerMajor, int protoVerMinor
// begin outgoing (i. e., from the app) command
#define LLC_APP_CMD_BEGIN 1 // params: int unit_id, int total_cmd_len
// a chunk of outgoing command, none is sent for zero length cmd
#define LLC_APP_CMD_CHUNK 2 // params: int unit_id, data...
// cancel of outgoing command
// (no 'done' llc, it'll be automatically as enough bytes are sent)
#define LLC_APP_CMD_CANCEL 3 // params: int unit_id
// request the client to download a file from the app
#define LLC_APP_FILE_DNLD 4 // params: int size, sugg_name/filter...
// request the client to upload a file into the app
#define LLC_APP_FILE_UPLD 5	// params: sugg_name/filter...
// next chunk of downloaded file sent to the client
#define LLC_APP_FILE_CHUNK 6 // params: data...
// cancel of downloading or uploading file by app's initiative
#define LLC_APP_FILE_CANCEL 7 // params: none
// command to open window for a unit
#define LLC_APP_WINDOW_OPEN 8 // params: int unit_id, url...
// command to close window for a unit
#define LLC_APP_WINDOW_CLOSE 9 // params: int unit_id

// client side:
// say hello, an obsolete command that does nothing, can also
// be possibly used for heartbeat ping
#define LLC_CLT_HELLO 0 // params: none
// begin incoming (i. e, into the app) command
#define LLC_CLT_CMD_BEGIN 1 // params: int unit_id, int total_cmd_len
// a chunk of incoming command, none is sent for zero length cmd
#define LLC_CLT_CMD_CHUNK 2  // params: int unit_id, data...
// cancel of incoming command
// (no 'done' llc, it'll be automatically as enough bytes are received)
#define LLC_CLT_CMD_CANCEL 3 // params: int unit_id
// confirmation to the client of statring downloading a file
#define LLC_CLT_ACK_FILE_DNLD 4 // params: user_name...
// confirmation to the client of statring uploading a file
#define LLC_CLT_ACK_FILE_UPLD 5 // params: int size,user_name...
// next chunk of uploaded file sent to the app
#define LLC_CLT_FILE_CHUNK 6 // params: data...
// cancel of downloading or uploading file by client's initiative
#define LLC_CLT_FILE_CANCEL 7 // params: none
// unit ready (invoked htmdUnit.turnIn), the client is not guaranteed
// to handle app commands until this notification
#define LLC_CLT_UNIT_TURN_IN 8 // params: int unit_id
// unit suspended (user closed unit window, reloaded or navigated
// elsewhere), the client is not guaranteed to handle app commands
// after this event and until LLC_CLT_UNIT_TURN_IN
#define LLC_CLT_UNIT_SUSPEND 9 // params: int unit_id

class HtMdLowLevelAppCmd
{
	friend class HtMdSupervisor;

protected:
	virtual ~HtMdLowLevelAppCmd () {}
	virtual bool requiresUnit (HtMdUnit *) { return false; }
	virtual bool requiresUploader (HtMdFileUploader *)
	{ return false; }
	virtual bool requiresDownloader (HtMdFileDownloader *)
	{ return false; }
	virtual int build (HtMdChunkDesc&) = 0; // return number of bytes taken
};

// extract big-endian 32-bit uint stored at pBytes
static unsigned extractUint (const char *pBytes)
{
	unsigned char *pData = (unsigned char *)pBytes;
	return (unsigned (pData[0])<<24)+
		(unsigned (pData[1])<<16)+
		(unsigned (pData[2])<<8)+
		(unsigned (pData[3]));
}

static void serializeUint (char *pBytes,unsigned theUint)
{
	unsigned char *pData = (unsigned char *)pBytes;
	pData[0] = (theUint>>24)&0xFF;
	pData[1] = (theUint>>16)&0xFF;
	pData[2] = (theUint>>8)&0xFF;
	pData[3] = (theUint)&0xFF;
}

// LLC_APP_HELLO
class HtMdLLCAppHello: public HtMdLowLevelAppCmd
{
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=24);
		serializeUint (&dst[0],LLC_APP_HELLO);
		dst[4] = 'H';
		dst[5] = 'T';
		dst[6] = 'M';
		dst[7] = 'D';
		serializeUint (&dst[8],HTMD_MAX_PACKET_SIZE);
		serializeUint (&dst[12],HTMD_PROTO_VER_MAJOR);
		serializeUint (&dst[16],HTMD_PROTO_VER_MINOR);
		serializeUint (&dst[20],HTMD_PROTO_VER_REVISION);
		return 24;
	}
};

// LLC_APP_CMD_BEGIN
class HtMdLLCAppCmdBegin: public HtMdLowLevelAppCmd
{
	HtMdUnit *mpUnit;
	int mCmdSize;

	bool requiresUnit (HtMdUnit *pUnit) { return pUnit==mpUnit; }
	int build (HtMdChunkDesc &dst);

public:
	HtMdLLCAppCmdBegin (HtMdUnit *pUnit,int cmdSize):
		mpUnit (pUnit), mCmdSize (cmdSize) {}
};

// LLC_APP_CMD_CANCEL
class HtMdLLCAppCmdCancel: public HtMdLowLevelAppCmd
{
	int mUnitId;

	bool requiresUnit (HtMdUnit *pUnit) { return false; }
	// ^this command only carries unit id
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=8);
		serializeUint (&dst[0],LLC_APP_CMD_BEGIN);
		serializeUint (&dst[4],mUnitId);
		return 8;
	}

public:
	HtMdLLCAppCmdCancel (HtMdUnit *pUnit):
		mUnitId (pUnit->mUnitId) {}
};

// LLC_APP_CMD_CHUNK
class HtMdLLCAppCmdChunk: public HtMdLowLevelAppCmd
{
	HtMdUnit *mpUnit;

	bool requiresUnit (HtMdUnit *pUnit) { return pUnit==mpUnit; }
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=12);
		serializeUint (&dst[0],LLC_APP_CMD_CHUNK);
		serializeUint (&dst[4],mpUnit->mUnitId);
		HtMdChunkDesc dstData (&dst[8],dst.getLength ()-8);
		int outBytes = 8+mpUnit->outUnitCommandChunk (dstData);
		mpUnit->mUnitCommandLenSofar += outBytes-8;
		if (mpUnit->mUnitCommandLenSofar<mpUnit->mUnitCommandLenTotal &&
			mpUnit->mUnitCommandInProgress)
			// ^could be cancelled in outUnitCommandChunk
		{
			// more data - schedule next LLC_APP_CMD_CHUNK
			HtMdSupervisor::spInstance->mOutLLCs.push_back
				(new HtMdLLCAppCmdChunk (mpUnit));
		}
		else
		{
			// data over
			bool wasCancelled = !mpUnit->mUnitCommandInProgress;
			mpUnit->mUnitCommandInProgress = false;
			if (!wasCancelled) mpUnit->outUnitCommandDone ();
		}
		return outBytes;
	}

public:
	HtMdLLCAppCmdChunk (HtMdUnit *pUnit): mpUnit (pUnit) {}
};

int HtMdLLCAppCmdBegin::build (HtMdChunkDesc &dst)
{
	assert (dst.getLength ()>=12);
	serializeUint (&dst[0],LLC_APP_CMD_BEGIN);
	serializeUint (&dst[4],mpUnit->mUnitId);
	serializeUint (&dst[8],mCmdSize);
	if (mpUnit->mUnitCommandLenTotal==0)
	{
		// 0 len data, no LLC_APP_CMD_CHUNKs required
		mpUnit->mUnitCommandInProgress = false;
		mpUnit->outUnitCommandDone ();
	}
	else
	{
		// there will be data, schedule a LLC_APP_CMD_CHUNK
		HtMdSupervisor::spInstance->mOutLLCs.push_back
			(new HtMdLLCAppCmdChunk (mpUnit));
	}
	return 12;
}

// LLC_APP_WINDOW_OPEN
class HtMdLLCAppWindowOpen: public HtMdLowLevelAppCmd
{
	int mUnitId;
	std::string mUrl;

	bool requiresUnit (HtMdUnit *pUnit) { return false; }
	// ^this command only carries unit id
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=8);
		serializeUint (&dst[0],LLC_APP_WINDOW_OPEN);
		serializeUint (&dst[4],mUnitId);
		int urlLen = (int)mUrl.length ();
		if (urlLen>dst.getLength ()-8)
			urlLen = dst.getLength ()-8;
		for (int i=0; i<urlLen; i++)
			dst[8+i] = mUrl[i];
		return 8+urlLen;
	}

public:
	HtMdLLCAppWindowOpen (HtMdUnit *pUnit,const std::string &url):
		mUnitId (pUnit->mUnitId), mUrl (url) {}
};

// LLC_APP_WINDOW_CLOSE
class HtMdLLCAppWindowClose: public HtMdLowLevelAppCmd
{
	int mUnitId;

	bool requiresUnit (HtMdUnit *pUnit) { return false; }
	// ^this command only carries unit id and is not withdrawable
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=8);
		serializeUint (&dst[0],LLC_APP_WINDOW_CLOSE);
		serializeUint (&dst[4],mUnitId);
		return 8;
	}

public:
	HtMdLLCAppWindowClose (HtMdUnit *pUnit):
		mUnitId (pUnit->mUnitId) {}
};

// LLC_APP_FILE_UPLD
class HtMdLLCFileUpload: public HtMdLowLevelAppCmd
{
	HtMdFileUploader *mpUploader;
	std::string mSuggName;
	bool requiresUploader (HtMdFileUploader *pUploader)
	{ return mpUploader==pUploader; }
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=4);
		serializeUint (&dst[0],LLC_APP_FILE_UPLD);
		unsigned u,n;
		for (u=0, n=mSuggName.size (),
			n=n>(unsigned)dst.getLength ()-4
				? dst.getLength ()-4 : n;
			u<n; u++)
			dst[u+4] = mSuggName[u];
		// once the command is dispatched it means
		// that uploader went into waiting-ack state
		mpUploader->mState = HtMdFileUploader::ST_WAITING_ACK;
		return 4+n;
	}

public:
	HtMdLLCFileUpload (HtMdFileUploader *pUploader):
		mpUploader (pUploader),
		mSuggName (pUploader->mSuggName)
	{}
};

class HtMdLLCFileCancel: public HtMdLowLevelAppCmd
{
	// this command requires no extra information at all
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=4);
		serializeUint (&dst[0],LLC_APP_FILE_CANCEL);
		return 4;
	}

public:
};

// LLC_APP_FILE_DNLD
class HtMdLLCFileDownload: public HtMdLowLevelAppCmd
{
	HtMdFileDownloader *mpDownloader;
	std::string mSuggName;
	int mSize;
	bool requiresDownloader (HtMdFileDownloader *pDownloader)
	{ return mpDownloader==pDownloader; }
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=8);
		serializeUint (&dst[0],LLC_APP_FILE_DNLD);
		serializeUint (&dst[4],mSize);
		unsigned u,n;
		for (u=0, n=mSuggName.size (),
			n=n>(unsigned)dst.getLength ()-8
				? dst.getLength ()-8 : n;
			u<n; u++)
			dst[u+8] = mSuggName[u];
		// once the command is dispatched it means
		// that uploader went into waiting-ack state
		mpDownloader->mState = HtMdFileDownloader::ST_WAITING_ACK;
		return 8+n;
	}

public:
	HtMdLLCFileDownload (HtMdFileDownloader *pDownloader):
		mpDownloader (pDownloader),
		mSuggName (pDownloader->mSuggName),
		mSize (mpDownloader->mDataTotal)
	{}
};

// LLC_APP_FILE_CHUNK
class HtMdLLCFileChunk: public HtMdLowLevelAppCmd
{
	HtMdFileDownloader *mpDownloader;
	bool requiresDownloader (HtMdFileDownloader *pDownloader)
	{ return mpDownloader==pDownloader; }
	int build (HtMdChunkDesc &dst)
	{
		assert (dst.getLength ()>=4);
		serializeUint (&dst[0],LLC_APP_FILE_CHUNK);
		HtMdChunkDesc data = dst.Subchunk (4);
		int bytesToFetch =
			mpDownloader->mDataTotal-mpDownloader->mDataSofar;
		if (bytesToFetch>data.getLength ())
			bytesToFetch = data.getLength ();
		mpDownloader->inDownloadChunk (data.Subchunk (0,bytesToFetch));
		if (!mpDownloader->isInProgress ())
			return 0; // cancelled from inside inDownload chunk,
			// the packet must not be sent
		mpDownloader->mDataSofar += bytesToFetch;
		if (mpDownloader->mDataSofar>=mpDownloader->mDataTotal)
		{
			// file completely sent out - discard all its roundabouts
			HtMdSupervisor::spInstance->dropDownloaderOutCommands
				(mpDownloader);
			if (HtMdSupervisor::spInstance->mpCurrentDownloader==
				mpDownloader)
				HtMdSupervisor::spInstance->mpCurrentDownloader = NULL;
			mpDownloader->mState = HtMdFileDownloader::ST_OK;
				mpDownloader->inDownloadOk ();
		}
		return 4+bytesToFetch;
	}

public:
	HtMdLLCFileChunk (HtMdFileDownloader *pDownloader):
		mpDownloader (pDownloader)
	{
		HtMdSupervisor::Lock htMdCritSec;
		HtMdSupervisor::spInstance->mDownloadFilePacketsPending++;
	}
	~HtMdLLCFileChunk ()
	{
		HtMdSupervisor::Lock htMdCritSec;
		HtMdSupervisor::spInstance->mDownloadFilePacketsPending--;
	}
};

//
// HtMdUnit
//

HtMdUnit::HtMdUnit ():
	mUnitWindowOpen (false),
	mClientTurnedIn (false),
	mClientCommandInProgress (false),
	mUnitCommandInProgress (false)
{
	HtMdSupervisor::Lock htMdCritSec;
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	mUnitId = (supvsr.mNextUnitId++)&0xFFFFFFFF; // enforce 32-bitness
	supvsr.mUnitsById[mUnitId] = this;
	supvsr.mNewUnits.insert (this);
}

HtMdUnit::~HtMdUnit ()
{
	HtMdSupervisor::Lock htMdCritSec;
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	outUnitCloseWindow ();
	supvsr.dropUnitOutCommands (this);
	supvsr.mUnitsById.erase (mUnitId);
	supvsr.mIdleSenderUnits.erase (this);

	supvsr.mNewUnits.erase (this);
}

void HtMdUnit::outUnitCommandBegin (int len)
{
	HtMdSupervisor::Lock htMdCritSec;
	outUnitCommandCancel ();
	mUnitCommandLenSofar = 0;
	mUnitCommandLenTotal = len;
	mUnitCommandInProgress = true;
	if (HtMdSupervisor::isConnected ())
		HtMdSupervisor::spInstance->mOutLLCs.push_back
			(new HtMdLLCAppCmdBegin (this,len));
	else
		// sent while unconnected, in to-cancel list
		HtMdSupervisor::spInstance->mIdleSenderUnits.insert (this);
}

void HtMdUnit::outUnitCommandCancel ()
{
	HtMdSupervisor::Lock htMdCritSec;
	if (mUnitCommandInProgress)
	{
		HtMdSupervisor::spInstance->dropUnitOutCommands (this);
		HtMdSupervisor::spInstance->mOutLLCs.push_back
			(new HtMdLLCAppCmdCancel (this));
		mUnitCommandInProgress = false;
	}
}

void HtMdUnit::outUnitOpenWindow (const std::string &url)
{
	HtMdSupervisor::Lock htMdCritSec;
	HtMdSupervisor::spInstance->mOutLLCs.push_back
		(new HtMdLLCAppWindowOpen (this,url));
	mClientTurnedIn = false;
	mUnitWindowOpen = true;
}

void HtMdUnit::outUnitCloseWindow ()
{
	HtMdSupervisor::Lock htMdCritSec;
	if (mUnitWindowOpen)
	{
		mUnitWindowOpen = false;
		HtMdSupervisor::spInstance->mOutLLCs.push_back
			(new HtMdLLCAppWindowClose (this));
		mClientTurnedIn = false;
	}
}

bool HtMdUnit::isConnected ()
{
	HtMdSupervisor::Lock htMdCritSec;
	return HtMdSupervisor::isConnected ();
}

//
// HtMdFileUploader
//

HtMdFileUploader::HtMdFileUploader ():
	mState (ST_OK),
	mDataTotal (-1),
	mDataSofar (-1)
{
	HtMdSupervisor::Lock htMdCritSec;
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	supvsr.mUploaders.insert (this);
}

HtMdFileUploader::~HtMdFileUploader ()
{
	HtMdSupervisor::Lock htMdCritSec;
	assert (mState==ST_OK || mState==ST_CANCELLED);
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	supvsr.dropUploaderOutCommands (this);
	supvsr.mUploaders.erase (this);
}

void HtMdFileUploader::cancel ()
{
	HtMdSupervisor::Lock htMdCritSec;
	if (!isInProgress ()) return;
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	supvsr.dropUploaderOutCommands (this);
	unsigned u,n;
	if (supvsr.mpCurrentUploader==this)
		supvsr.mpCurrentUploader = NULL;
	else // in queue
	for (u=0, n=supvsr.mUploadRequestQueue.size (); u<n; u++)
	{
		HtMdFileUploader *&pUploader = supvsr.mUploadRequestQueue[u];
		if (pUploader==this)
		{
			pUploader = NULL;
			break; // can't be enqueued more than once
		}
	}

	if (mState!=ST_QUEUED && mState!=ST_CLIENT_CANCEL_IN_PROGRESS)
		// request command is sent out, need to send cancel
		supvsr.mOutLLCs.push_back (new HtMdLLCFileCancel ());
	mState = ST_CANCELLED;

	this->inUploadCancelled ();
}

void HtMdFileUploader::beginRequestUpload
	(const std::string &nameOfFilter)
{
	HtMdSupervisor::Lock htMdCritSec;
	if (isInProgress ()) cancel ();
	HtMdSupervisor::spInstance->mUploadRequestQueue.push_back (this);
	mState = ST_QUEUED;
	mSuggName = nameOfFilter;
}

bool HtMdFileUploader::isInProgress ()
{
	HtMdSupervisor::Lock htMdCritSec;
	return mState!=ST_OK && mState!=ST_CANCELLED;
}

bool HtMdFileUploader::isFailed ()
{
	HtMdSupervisor::Lock htMdCritSec;
	return mState==ST_CANCELLED;
}

//
// HtMdFileDownloader
//

HtMdFileDownloader::HtMdFileDownloader ():
	mState (ST_OK),
	mDataTotal (-1),
	mDataSofar (-1)
{
	HtMdSupervisor::Lock htMdCritSec;
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	supvsr.mDownloaders.insert (this);
}

HtMdFileDownloader::~HtMdFileDownloader ()
{
	HtMdSupervisor::Lock htMdCritSec;
	assert (mState==ST_OK || mState==ST_CANCELLED);
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	supvsr.dropDownloaderOutCommands (this);
	supvsr.mDownloaders.erase (this);
}

void HtMdFileDownloader::cancel ()
{
	HtMdSupervisor::Lock htMdCritSec;
	if (!isInProgress ()) return;
	HtMdSupervisor &supvsr = *HtMdSupervisor::spInstance;

	supvsr.dropDownloaderOutCommands (this);
	unsigned u,n;
	if (supvsr.mpCurrentDownloader==this)
		supvsr.mpCurrentDownloader = NULL;
	else // in queue
	for (u=0, n=supvsr.mDownloadRequestQueue.size (); u<n; u++)
	{
		HtMdFileDownloader *&pDownloader =
			supvsr.mDownloadRequestQueue[u];
		if (pDownloader==this)
		{
			pDownloader = NULL;
			break; // can't be enqueued more than once
		}
	}

	if (mState!=ST_QUEUED && mState!=ST_CLIENT_CANCEL_IN_PROGRESS)
		// request command is sent out, need to send cancel
		supvsr.mOutLLCs.push_back (new HtMdLLCFileCancel ());
	mState = ST_CANCELLED;

	this->inDownloadCancelled ();
}

void HtMdFileDownloader::beginRequestDownload
	(const std::string &nameOfFilter,int size)
{
	HtMdSupervisor::Lock htMdCritSec;
	if (isInProgress ()) cancel ();
	HtMdSupervisor::spInstance->mDownloadRequestQueue.push_back (this);
	mState = ST_QUEUED;
	mSuggName = nameOfFilter;
	mDataTotal = size;
	mDataSofar = 0;
}

bool HtMdFileDownloader::isInProgress ()
{
	HtMdSupervisor::Lock htMdCritSec;
	return mState!=ST_OK && mState!=ST_CANCELLED;
}

bool HtMdFileDownloader::isFailed ()
{
	HtMdSupervisor::Lock htMdCritSec;
	return mState==ST_CANCELLED;
}

//
// HtMdSupervisor
//

HtMdSupervisor *HtMdSupervisor::spInstance = NULL;

HtMdSupervisor::HtMdSupervisor (HtMdConnection &connection):
	mConnection (connection),
	mInChunkInProgress (false),
	mOutChunkInProgress (false),
	mNextUnitId (0),
	mpCurrentUploader (NULL),
	mpCurrentDownloader (NULL),
	mDownloadFilePacketsPending (0)
{
	assert (spInstance==NULL);
	spInstance = this;
}

HtMdSupervisor::~HtMdSupervisor ()
{
	assert (spInstance==this);
	spInstance = NULL;
}

bool HtMdSupervisor::isConnected ()
{
	HtMdSupervisor::Lock htMdCritSec;
	return spInstance->mConnection.getState ()==HtMdConnection::CONNECTED;
}

bool HtMdSupervisor::isDefunct ()
{
	HtMdSupervisor::Lock htMdCritSec;
	return spInstance->mConnection.getState ()==HtMdConnection::DEFUNCT;
}

static bool gInsideUpdate = false;
void HtMdSupervisor::update ()
{
	assert (spInstance!=NULL);
	HtMdSupervisor &self = *spInstance;

	if (!self.mConnection.tryRecursiveLock ())
		return; // already execed in another thread

	// scope-cleanup unlocker
	struct UnlockGuard
	{
		~UnlockGuard ()
		{ spInstance->mConnection.recursiveUnlock (); }
	} unlockGuard;
	(void)unlockGuard; // NOT unused

	if (gInsideUpdate) return; // no reenterance
	gInsideUpdate = true;

	// scope-cleanup reenterance flag resetter
	struct ReEntryGuard
	{
		bool &mTheFlag;
		ReEntryGuard (bool &theFlag): mTheFlag (theFlag) {}
		~ReEntryGuard () { mTheFlag = false; }
	} reEnteryGuard (gInsideUpdate);
	(void)reEnteryGuard; // NOT unused

	if (self.mConnection.getState ()!=HtMdConnection::DEFUNCT)
	{
		// ping updates
		std::map<int,HtMdUnit *>::iterator it;
		for (it=self.mUnitsById.begin ();
			it!=self.mUnitsById.end (); ++it)
			it->second->onUpdate ();
	}

	int packetsProcessed = 0;
	switch (self.mConnection.getState ())
	{
	case HtMdConnection::NOT_CONNECTED:
		// cleanup units that sent command before connection
		while (!self.mIdleSenderUnits.empty ())
		{
			HtMdUnit *pUnit = *self.mIdleSenderUnits.begin ();
			self.mIdleSenderUnits.erase (pUnit);
			pUnit->outUnitCommandCancel ();
		}
		// cleanup uploaders launched before connection
		while (!self.mUploadRequestQueue.empty ())
		{
			HtMdFileUploader *pUploader =
				*self.mUploadRequestQueue.begin ();
			if (pUploader!=NULL)
				pUploader->cancel (); // will also deque it
			else // NULLs must be dequeued manually
				self.mUploadRequestQueue.pop_front ();
		}
		// cleanup downloaders launched before connection
		while (!self.mDownloadRequestQueue.empty ())
		{
			HtMdFileDownloader *pDownloader =
				*self.mDownloadRequestQueue.begin ();
			if (pDownloader!=NULL)
				pDownloader->cancel (); // will also deque it
			else // NULLs must be dequeued manually
				self.mDownloadRequestQueue.pop_front ();
		}
		self.mConnection.pollForInboundConnection ();
		if (self.mConnection.getState ()==HtMdConnection::CONNECTED)
		{
			// reset chunk reader/writer
			self.mInChunkLenSofar =
			self.mInChunkDataSofar =
			self.mInChunkDataTotal = 0;
			self.mInChunkInProgress = false;
			self.mOutChunkLenSofar =
			self.mOutChunkDataSofar =
			self.mOutChunkDataTotal = 0;
			self.mOutChunkInProgress = false;
			self.mOutChunkPrepared = false;

			// no more need in this list, we'll notify everyone now
			self.mNewUnits.clear ();

			// cleanup outgoing commands, they are inappropriate
			self.dropAllOutCommands ();

			// first of all, tell the client our parameters
			self.mOutLLCs.push_back (new HtMdLLCAppHello ());

			// tell people that we got a connect
			std::map<int,HtMdUnit *>::iterator it;
			for (it=self.mUnitsById.begin ();
				it!=self.mUnitsById.end (); ++it)
			{
				it->second->onClientConnected ();
			}
		}
		return;

	case HtMdConnection::CONNECTED:
		while (!self.mNewUnits.empty ())
		{
			// notify newly created units of the connection
			HtMdUnit *pNewUnit = *self.mNewUnits.begin ();
			self.mNewUnits.erase (pNewUnit);
			pNewUnit->onClientConnected ();
		}
	OR_MORE_INPUT: // possibly more chunks are awaiting?
		if (!self.mInChunkInProgress)
		{
			// begin polling next input chunk
			// (do as soon as possible since we don't
			// know when the client may send the data)
			self.mInChunkLenSofar = self.mInChunkDataSofar =
				self.mInChunkDataTotal = 0;
			self.mInChunkInProgress = true;
		}
		if (self.mInChunkInProgress)
		{
			// input chunk is being read
			if (self.mInChunkLenSofar<4)
			{
				self.mInChunkLenSofar += self.mConnection.tryRead (
					HtMdChunkDesc (
						self.mInChunkLen+self.mInChunkLenSofar,
						4-self.mInChunkLenSofar));
				if (self.mInChunkLenSofar>=4)
				{
					self.mInChunkDataTotal = extractUint
						(self.mInChunkLen);
					self.mInChunkDataSofar = 0;
				}
			}
			if (self.mInChunkLenSofar>=4)
			{
				self.mInChunkDataSofar +=
					self.mConnection.tryRead (
					HtMdChunkDesc (
						self.mInChunkData+self.mInChunkDataSofar,
						self.mInChunkDataTotal-self.mInChunkDataSofar
					));
			}
			if (self.mInChunkDataSofar>=self.mInChunkDataTotal)
				self.mInChunkInProgress = false; // in-chunk ready
		}
		if (!isConnected ()) goto HANDLE_DISCONNECT;

		// an input chunk ready?
		if (!self.mInChunkInProgress && self.mInChunkLenSofar>=4)
		{
			self.handleInputLLC ();
			if (!isConnected ()) goto HANDLE_DISCONNECT;
			packetsProcessed++;
			goto OR_MORE_INPUT;
		}

		// writing output chunk
	OR_MORE_OUTPUT: // (possibly another one in this update)
		if (!self.mOutChunkInProgress && self.mOutChunkPrepared)
		{
			// begin wrtiting next output chunk
			// (do as soon as possible since we don't
			// know when the client may send the data)
			self.mOutChunkLenSofar = self.mOutChunkDataSofar = 0;
			// the size to write is in mOutChunkDataTotal, prepare it
			serializeUint (self.mOutChunkLen,self.mOutChunkDataTotal);
			self.mOutChunkInProgress = true;
			self.mOutChunkPrepared = false;
		}
		if (self.mOutChunkInProgress)
		{
			// output chunk is being written
			if (self.mOutChunkLenSofar<4 && self.mOutChunkDataTotal>0)
			{
				self.mOutChunkLenSofar += self.mConnection.tryWrite (
					HtMdConstChunkDesc (
						self.mOutChunkLen+self.mOutChunkLenSofar,
						4-self.mOutChunkLenSofar));
				if (self.mOutChunkLenSofar>=4)
					self.mOutChunkDataSofar = 0;
			}
			if (self.mOutChunkLenSofar>=4 && self.mOutChunkDataTotal>0)
			{
				self.mOutChunkDataSofar +=
					self.mConnection.tryWrite (
					HtMdConstChunkDesc (
						self.mOutChunkData+self.mOutChunkDataSofar,
						self.mOutChunkDataTotal-self.mOutChunkDataSofar
					));
			}
			if (self.mOutChunkDataSofar>=self.mOutChunkDataTotal)
			{
				self.mOutChunkInProgress = false; // out-chunk ready
			}
		}
		if (!self.mOutChunkInProgress)
		{
			// output has been completed, why not try to do more
			self.prepareMoreOutput ();
			if (self.mOutChunkPrepared) goto OR_MORE_OUTPUT;
		}

		// file downloaders queue process
		if (self.mpCurrentDownloader==NULL)
		{
			// pick 1st non-null from the queue
			while (self.mpCurrentDownloader==NULL &&
				!self.mDownloadRequestQueue.empty ())
			{
				self.mpCurrentDownloader =
					self.mDownloadRequestQueue.front ();
				self.mDownloadRequestQueue.pop_front ();
			}
			// submit request command
			if (self.mpCurrentDownloader!=NULL)
				self.mOutLLCs.push_back (new HtMdLLCFileDownload
				(self.mpCurrentDownloader));
		}

		// file uploaders queue process (but downloaders have priority)
		if (self.mpCurrentDownloader==NULL &&
			self.mpCurrentUploader==NULL)
		{
			// pick 1st non-null from the queue
			while (self.mpCurrentUploader==NULL &&
				!self.mUploadRequestQueue.empty ())
			{
				self.mpCurrentUploader =
					self.mUploadRequestQueue.front ();
				self.mUploadRequestQueue.pop_front ();
			}
			// submit request command
			if (self.mpCurrentUploader!=NULL)
				self.mOutLLCs.push_back (new HtMdLLCFileUpload
				(self.mpCurrentUploader));
		}

		// possibly process current download
		if (self.mpCurrentDownloader!=NULL &&
			self.mpCurrentDownloader->mState==
			HtMdFileDownloader::ST_WAITING_DATA)
		{
			// just stuff file chunk packets until assumed size of
			// pending chunks reaches MAX_DNLD_FILE_PENDING_BYTES,
			// surplus packets will be culled later on processing of
			// HtMdLLCAppFileChunk::build
			while (self.mDownloadFilePacketsPending
				*(HTMD_MAX_PACKET_SIZE-4)
				<HTMD_MAX_DNLD_FILE_PENDING_BYTES)
				self.mOutLLCs.push_back (new HtMdLLCFileChunk
					(self.mpCurrentDownloader));
		}
		return;

	HANDLE_DISCONNECT:
		{
			// reset commands queue
			self.dropAllOutCommands ();

			// tell people that we got a disconnect
			std::map<int,HtMdUnit *>::iterator it;
			for (it=self.mUnitsById.begin ();
				it!=self.mUnitsById.end (); ++it)
			{
				HtMdUnit *pUnit = it->second;
				pUnit->outUnitCloseWindow ();
				pUnit->outUnitCommandCancel ();
				if (pUnit->mClientCommandInProgress)
				{
					pUnit->mClientCommandInProgress = false;
					pUnit->inClientCommandCancelled ();
				}
				pUnit->mClientTurnedIn = false;
				pUnit->onClientDisconnected ();
			}
			self.mNewUnits.clear ();

			// cancel all uploads
			std::set<HtMdFileUploader *>::iterator upldIt;
			for (upldIt=self.mUploaders.begin ();
				upldIt!=self.mUploaders.end (); ++upldIt)
				(*upldIt)->cancel ();
			self.mUploadRequestQueue.clear ();
			// ^to clear possible after-cancellation NULL stubs

			// similarly, cancel all downloads
			std::set<HtMdFileDownloader *>::iterator dnldIt;
			for (dnldIt=self.mDownloaders.begin ();
				dnldIt!=self.mDownloaders.end (); ++dnldIt)
				(*dnldIt)->cancel ();
			self.mDownloadRequestQueue.clear ();
		}
		return;

	case HtMdConnection::DEFUNCT:
		// nothing we can do in this case
		break;
	}
}

HtMdUnit *HtMdSupervisor::getUnitById (int unitId)
{
	std::map<int,HtMdUnit *>::iterator it = mUnitsById.find (unitId);
	return it!=mUnitsById.end ()? it->second : NULL;
}

void HtMdSupervisor::handleInputLLC ()
{
	if (mInChunkDataTotal>=4)
	{
		unsigned commandId = extractUint (mInChunkData+0);
		HtMdUnit *pUnit;
		switch (commandId)
		{
		case LLC_CLT_CMD_BEGIN:
			if (mInChunkDataTotal<12) goto IGNORABLE_INVALID_COMMAND;
			pUnit = getUnitById ((int)extractUint (mInChunkData+4));
			if (pUnit==NULL) goto IGNORABLE_INVALID_COMMAND;
			if (pUnit->mClientCommandInProgress)
			{
				// beginning of new command before old one finished
				// is treated as automatic cancelling old one
				pUnit->mClientCommandInProgress = false;
				pUnit->inClientCommandCancelled ();
			}
			pUnit->mClientCommandInProgress = true;
			pUnit->mClientCommandLenSofar = 0;
			pUnit->mClientCommandLenTotal = (int)extractUint (mInChunkData+8);
			pUnit->inClientCommandBegin (pUnit->mClientCommandLenTotal);
			if (pUnit->mClientCommandLenTotal==0)
			{
				// zero len command - no more packets will be for it,
				// exec it at once
				pUnit->mClientCommandInProgress = false;
				pUnit->inClientCommandOk ();
			}
			return;
		case LLC_CLT_CMD_CHUNK:
			if (mInChunkDataTotal<8) goto IGNORABLE_INVALID_COMMAND;
			pUnit = getUnitById ((int)extractUint (mInChunkData+4));
			if (pUnit==NULL) goto IGNORABLE_INVALID_COMMAND;
			if (pUnit->mClientCommandInProgress)
			{
				// bytes left to complete the command
				unsigned bytes = pUnit->mClientCommandLenTotal-
					pUnit->mClientCommandLenSofar;
				// bytes of them we can read from this chunk
				if (bytes>mInChunkDataTotal-8)
					bytes = mInChunkDataTotal-8;
				pUnit->mClientCommandLenSofar += bytes;
				pUnit->inClientCommandChunk (
					HtMdConstChunkDesc (mInChunkData+8,bytes));
				if (pUnit->mClientCommandLenSofar>=
					pUnit->mClientCommandLenTotal)
				{
					pUnit->mClientCommandInProgress = false;
					pUnit->inClientCommandOk ();
				}
			}
			return;
		case LLC_CLT_CMD_CANCEL:
			if (mInChunkDataTotal<8) goto IGNORABLE_INVALID_COMMAND;
			pUnit = getUnitById ((int)extractUint (mInChunkData+4));
			if (pUnit==NULL) goto IGNORABLE_INVALID_COMMAND;
			if (pUnit->mClientCommandInProgress)
			{
				pUnit->mClientCommandInProgress = false;
				pUnit->inClientCommandCancelled ();
			}
			return;
		case LLC_CLT_ACK_FILE_UPLD:
			if (mpCurrentUploader!=NULL)
			{
				if (mInChunkDataTotal<8) goto IGNORABLE_INVALID_COMMAND;
				mpCurrentUploader->mDataTotal =
					(int)extractUint (mInChunkData+4);
				mpCurrentUploader->mDataSofar = 0;
				std::string actualName (mInChunkData+8,
					mInChunkDataTotal-8);
				mpCurrentUploader->mState =
					HtMdFileUploader::ST_WAITING_DATA;
				mpCurrentUploader->inUploadBegin (actualName,
					mpCurrentUploader->mDataTotal);
				if (mpCurrentUploader->mDataTotal<=0)
				{
					// empty file, no chunks are expected, complete now
					HtMdFileUploader *pCurUploader = mpCurrentUploader;
					mpCurrentUploader = NULL;
					pCurUploader->mState = HtMdFileUploader::ST_OK;
					pCurUploader->inUploadOk ();
				}
			}
			return;
		case LLC_CLT_ACK_FILE_DNLD:
			if (mpCurrentDownloader!=NULL)
			{
				std::string actualName (mInChunkData+4,
					mInChunkDataTotal-4);
				mpCurrentDownloader->mState =
					HtMdFileDownloader::ST_WAITING_DATA;
				mpCurrentDownloader->inDownloadBegin (actualName);
				if (mpCurrentDownloader->mDataTotal<=0)
				{
					// empty file, no chunks to send, complete now
					HtMdFileDownloader *pCurDownloader =
						mpCurrentDownloader;
					mpCurrentDownloader = NULL;
					pCurDownloader->mState = HtMdFileDownloader::ST_OK;
					pCurDownloader->inDownloadOk ();
				}
			}
			return;
		case LLC_CLT_FILE_CHUNK: // it's uploading only cmd
			if (mpCurrentUploader!=NULL)
			{
				// if got here, the command can't be invalid
				HtMdFileUploader *pCurUploader = mpCurrentUploader;
				unsigned n = pCurUploader->mDataTotal-
					pCurUploader->mDataSofar;
				if (mInChunkDataTotal-4<n) n = mInChunkDataTotal-4;
				pCurUploader->mDataSofar += n;
				pCurUploader->inUploadChunk (
					HtMdConstChunkDesc (mInChunkData+4,n));
				if (pCurUploader->mDataSofar>=
					pCurUploader->mDataTotal)
				{
					// download completed
					mpCurrentUploader = NULL;
					pCurUploader->mState = HtMdFileUploader::ST_OK;
					pCurUploader->inUploadOk ();
				}
			}
			return;
		case LLC_CLT_FILE_CANCEL:
			// cancel uploader if it's an uploading operation
			if (mpCurrentUploader!=NULL)
			{
				mpCurrentUploader->mState =
					HtMdFileUploader::ST_CLIENT_CANCEL_IN_PROGRESS;
				// ^to suppress LLC_APP_FILE_CANCEL command
				mpCurrentUploader->cancel ();
			}
			// cancel downloader if it's an downloading operation
			if (mpCurrentDownloader!=NULL)
			{
				mpCurrentDownloader->mState =
					HtMdFileDownloader::ST_CLIENT_CANCEL_IN_PROGRESS;
				// ^to suppress LLC_APP_FILE_CANCEL command
				mpCurrentDownloader->cancel ();
			}
			return;
		case LLC_CLT_UNIT_TURN_IN:
			if (mInChunkDataTotal<8) goto IGNORABLE_INVALID_COMMAND;
			pUnit = getUnitById ((int)extractUint (mInChunkData+4));
			if (pUnit==NULL) goto IGNORABLE_INVALID_COMMAND;
			if (!pUnit->mClientTurnedIn)
			{
				pUnit->mClientTurnedIn = true;
				pUnit->onClientTurnIn ();
			}
			return;
		case LLC_CLT_UNIT_SUSPEND:
			if (mInChunkDataTotal<8) goto IGNORABLE_INVALID_COMMAND;
			pUnit = getUnitById ((int)extractUint (mInChunkData+4));
			if (pUnit==NULL) goto IGNORABLE_INVALID_COMMAND;
			if (pUnit->mClientTurnedIn)
			{
				pUnit->mClientTurnedIn = false;
				pUnit->onClientSuspended ();
			}
			return;
		default:;
			// yet unhandled type of command
		}
	}
IGNORABLE_INVALID_COMMAND:;
}

void HtMdSupervisor::prepareMoreOutput ()
{
	// fetch next outgoing command and dispatch it into the sender
SKIP_NULL:
	if (!mOutLLCs.empty ())
	{
		HtMdLowLevelAppCmd *pCmd = mOutLLCs.front ();
		mOutLLCs.pop_front ();
		if (pCmd==NULL) goto SKIP_NULL;
		HtMdChunkDesc outData (mOutChunkData,HTMD_MAX_PACKET_SIZE);
		mOutChunkDataTotal = pCmd->build (outData);
		delete pCmd;
		mOutChunkPrepared = true;
	}

	// output for unit commands are already in queue, see
	// HtMdLLCApp... implementations, so do nothing here about it
}

void HtMdSupervisor::dropUnitOutCommands (HtMdUnit *pUnit)
{
	unsigned u,n;
	for (u=0, n=mOutLLCs.size (); u<n; u++)
	{
		HtMdLowLevelAppCmd *&pCmd = mOutLLCs[u];
		if (pCmd==NULL) continue;
		if (pCmd->requiresUnit (pUnit))
		{
			delete pCmd;
			pCmd = NULL;
		}
	}
}

void HtMdSupervisor::dropAllOutCommands ()
{
	while (!mOutLLCs.empty ())
	{
		if (mOutLLCs.front ()!=NULL) delete mOutLLCs.front ();
		mOutLLCs.pop_front ();
	}
}

void HtMdSupervisor::dropUploaderOutCommands
	(HtMdFileUploader *pUploader)
{
	unsigned u,n;
	for (u=0, n=mOutLLCs.size (); u<n; u++)
	{
		HtMdLowLevelAppCmd *&pCmd = mOutLLCs[u];
		if (pCmd==NULL) continue;
		if (pCmd->requiresUploader (pUploader))
		{
			delete pCmd;
			pCmd = NULL;
		}
	}
}

void HtMdSupervisor::dropDownloaderOutCommands
	(HtMdFileDownloader *pDownloader)
{
	unsigned u,n;
	for (u=0, n=mOutLLCs.size (); u<n; u++)
	{
		HtMdLowLevelAppCmd *&pCmd = mOutLLCs[u];
		if (pCmd==NULL) continue;
		if (pCmd->requiresDownloader (pDownloader))
		{
			delete pCmd;
			pCmd = NULL;
		}
	}
}

//
// lock
//

HtMdSupervisor::Lock::Lock ()
{
	assert (HtMdSupervisor::spInstance!=NULL);
	HtMdSupervisor::spInstance->mConnection.recursiveLock ();
}

HtMdSupervisor::Lock::~Lock ()
{
	assert (HtMdSupervisor::spInstance!=NULL);
	HtMdSupervisor::spInstance->mConnection.recursiveUnlock ();
}

#endif // DISABLE_HTMD
